<?php

namespace gyh\GyhReadsheet\Cell;


use gyh\GyhReadsheet\Calculation\Calculation;
use gyh\GyhReadsheet\Collection\Cells;
use gyh\GyhReadsheet\Exception;
use gyh\GyhReadsheet\RichText\RichText;
use gyh\GyhReadsheet\Style\NumberFormat;
use gyh\GyhReadsheet\Style\Style;
use gyh\GyhReadsheet\Worksheet\Worksheet;
use gyh\GyhReadsheet\Shared\StringHelper;
use DateTimeInterface;

class Cell
{
    private static $valueBinder;

    private $value;

    private $calculatedValue;

    private $dataType;

    private $parent;

    private $xfIndex = 0;

    private $formulaAttributes;

    public function updateInCollection()
    {
        $this->parent->update($this);

        return $this;
    }

    public function detach()
    {
        $this->parent = null;
    }

    public function attach(Cells $parent)
    {
        $this->parent = $parent;
    }

    public function __construct($pValue, $pDataType, Worksheet $pSheet)
    {
        $this->value = $pValue;

        $this->parent = $pSheet->getCellCollection();

        if ($pDataType !== null) {
            if ($pDataType == DataType::TYPE_STRING2) {
                $pDataType = DataType::TYPE_STRING;
            }
            $this->dataType = $pDataType;
        } elseif (!self::getValueBinder()->bindValue($this, $pValue)) {
            throw new Exception('Value could not be bound to cell.');
        }
    }

    public function getColumn()
    {
        return $this->parent->getCurrentColumn();
    }

    public function getRow()
    {
        return $this->parent->getCurrentRow();
    }

    public function getCoordinate()
    {
        return $this->parent->getCurrentCoordinate();
    }

    public function getValue()
    {
        return $this->value;
    }

    public function getFormattedValue()
    {
        return (string) NumberFormat::toFormattedString(
            $this->getCalculatedValue(),
            $this->getStyle()
                ->getNumberFormat()->getFormatCode()
        );
    }

    public function setValue($pValue)
    {
        if (!self::getValueBinder()->bindValue($this, $pValue)) {
            throw new Exception('Value could not be bound to cell.');
        }

        return $this;
    }

    public function setValueExplicit($pValue, $pDataType)
    {
        switch ($pDataType) {
            case DataType::TYPE_NULL:
                $this->value = $pValue;

                break;
            case DataType::TYPE_STRING2:
                $pDataType = DataType::TYPE_STRING;
            case DataType::TYPE_STRING:
            case DataType::TYPE_INLINE:
                $this->value = DataType::checkString($pValue);

                break;
            case DataType::TYPE_NUMERIC:
                $this->value = (float) $pValue;

                break;
            case DataType::TYPE_FORMULA:
                $this->value = (string) $pValue;

                break;
            case DataType::TYPE_BOOL:
                $this->value = (bool) $pValue;

                break;
            case DataType::TYPE_ERROR:
                $this->value = DataType::checkErrorCode($pValue);

                break;
            default:
                throw new Exception('Invalid datatype: ' . $pDataType);

                break;
        }

        $this->dataType = $pDataType;

        return $this->updateInCollection();
    }

    public function getCalculatedValue($resetLog = true)
    {
        if ($this->dataType == DataType::TYPE_FORMULA) {
            try {
                $result = Calculation::getInstance(
                    $this->getWorksheet()->getParent()
                )->calculateCellValue($this, $resetLog);
                if (is_array($result)) {
                    while (is_array($result)) {
                        $result = array_pop($result);
                    }
                }
            } catch (Exception $ex) {
                if (($ex->getMessage() === 'Unable to access External Workbook') && ($this->calculatedValue !== null)) {
                    return $this->calculatedValue; // Fallback for calculations referencing external files.
                }

                throw new \gyh\GyhReadsheet\Calculation\Exception(
                    $this->getWorksheet()->getTitle() . '!' . $this->getCoordinate() . ' -> ' . $ex->getMessage()
                );
            }

            if ($result === '#Not Yet Implemented') {
                return $this->calculatedValue; // Fallback if calculation engine does not support the formula.
            }

            return $result;
        } elseif ($this->value instanceof RichText) {
            return $this->value->getPlainText();
        }

        return $this->value;
    }

    public function setCalculatedValue($pValue)
    {
        if ($pValue !== null) {
            $this->calculatedValue = (is_numeric($pValue)) ? (float) $pValue : $pValue;
        }

        return $this->updateInCollection();
    }

    public function getOldCalculatedValue()
    {
        return $this->calculatedValue;
    }

    public function getDataType()
    {
        return $this->dataType;
    }

    public function setDataType($pDataType)
    {
        if ($pDataType == DataType::TYPE_STRING2) {
            $pDataType = DataType::TYPE_STRING;
        }
        $this->dataType = $pDataType;

        return $this->updateInCollection();
    }

    public function isFormula()
    {
        return $this->dataType == DataType::TYPE_FORMULA;
    }

    public function hasDataValidation()
    {
        if (!isset($this->parent)) {
            throw new Exception('Cannot check for data validation when cell is not bound to a worksheet');
        }

        return $this->getWorksheet()->dataValidationExists($this->getCoordinate());
    }

    public function getDataValidation()
    {
        if (!isset($this->parent)) {
            throw new Exception('Cannot get data validation for cell that is not bound to a worksheet');
        }

        return $this->getWorksheet()->getDataValidation($this->getCoordinate());
    }

    public function setDataValidation(DataValidation $pDataValidation = null)
    {
        if (!isset($this->parent)) {
            throw new Exception('Cannot set data validation for cell that is not bound to a worksheet');
        }

        $this->getWorksheet()->setDataValidation($this->getCoordinate(), $pDataValidation);

        return $this->updateInCollection();
    }

    public function hasValidValue()
    {
        $validator = new DataValidator();

        return $validator->isValid($this);
    }

    public function hasHyperlink()
    {
        if (!isset($this->parent)) {
            throw new Exception('Cannot check for hyperlink when cell is not bound to a worksheet');
        }

        return $this->getWorksheet()->hyperlinkExists($this->getCoordinate());
    }

    public function getHyperlink()
    {
        if (!isset($this->parent)) {
            throw new Exception('Cannot get hyperlink for cell that is not bound to a worksheet');
        }

        return $this->getWorksheet()->getHyperlink($this->getCoordinate());
    }

    public function setHyperlink(Hyperlink $pHyperlink = null)
    {
        if (!isset($this->parent)) {
            throw new Exception('Cannot set hyperlink for cell that is not bound to a worksheet');
        }

        $this->getWorksheet()->setHyperlink($this->getCoordinate(), $pHyperlink);

        return $this->updateInCollection();
    }

    public function getParent()
    {
        return $this->parent;
    }

    public function getWorksheet()
    {
        return $this->parent->getParent();
    }

    public function isInMergeRange()
    {
        return (bool) $this->getMergeRange();
    }

    public function isMergeRangeValueCell()
    {
        if ($mergeRange = $this->getMergeRange()) {
            $mergeRange = Coordinate::splitRange($mergeRange);
            list($startCell) = $mergeRange[0];
            if ($this->getCoordinate() === $startCell) {
                return true;
            }
        }

        return false;
    }

    public function getMergeRange()
    {
        foreach ($this->getWorksheet()->getMergeCells() as $mergeRange) {
            if ($this->isInRange($mergeRange)) {
                return $mergeRange;
            }
        }

        return false;
    }

    public function getStyle()
    {
        return $this->getWorksheet()->getStyle($this->getCoordinate());
    }

    public function rebindParent(Worksheet $parent)
    {
        $this->parent = $parent->getCellCollection();

        return $this->updateInCollection();
    }

    public function isInRange($pRange)
    {
        list($rangeStart, $rangeEnd) = Coordinate::rangeBoundaries($pRange);

        $myColumn = Coordinate::columnIndexFromString($this->getColumn());
        $myRow = $this->getRow();

        return ($rangeStart[0] <= $myColumn) && ($rangeEnd[0] >= $myColumn) &&
                ($rangeStart[1] <= $myRow) && ($rangeEnd[1] >= $myRow);
    }

    public static function compareCells(self $a, self $b)
    {
        if ($a->getRow() < $b->getRow()) {
            return -1;
        } elseif ($a->getRow() > $b->getRow()) {
            return 1;
        } elseif (Coordinate::columnIndexFromString($a->getColumn()) < Coordinate::columnIndexFromString($b->getColumn())) {
            return -1;
        }

        return 1;
    }

    public static function getValueBinder()
    {
        if (self::$valueBinder === null) {
            self::$valueBinder = new DefaultValueBinder();
        }

        return self::$valueBinder;
    }

    public static function setValueBinder(IValueBinder $binder)
    {
        self::$valueBinder = $binder;
    }

    public function __clone()
    {
        $vars = get_object_vars($this);
        foreach ($vars as $key => $value) {
            if ((is_object($value)) && ($key != 'parent')) {
                $this->$key = clone $value;
            } else {
                $this->$key = $value;
            }
        }
    }

    public function getXfIndex()
    {
        return $this->xfIndex;
    }

    public function setXfIndex($pValue)
    {
        $this->xfIndex = $pValue;

        return $this->updateInCollection();
    }

    public function setFormulaAttributes($pAttributes)
    {
        $this->formulaAttributes = $pAttributes;

        return $this;
    }

    public function getFormulaAttributes()
    {
        return $this->formulaAttributes;
    }

    public function __toString()
    {
        return (string) $this->getValue();
    }
}


abstract class Coordinate
{
    const DEFAULT_RANGE = 'A1:A1';

    public static function coordinateFromString($pCoordinateString)
    {
        if (preg_match('/^([$]?[A-Z]{1,3})([$]?\\d{1,7})$/', $pCoordinateString, $matches)) {
            return [$matches[1], $matches[2]];
        } elseif (self::coordinateIsRange($pCoordinateString)) {
            throw new Exception('Cell coordinate string can not be a range of cells');
        } elseif ($pCoordinateString == '') {
            throw new Exception('Cell coordinate can not be zero-length string');
        }

        throw new Exception('Invalid cell coordinate ' . $pCoordinateString);
    }

    public static function coordinateIsRange($coord)
    {
        return (strpos($coord, ':') !== false) || (strpos($coord, ',') !== false);
    }

    public static function absoluteReference($pCoordinateString)
    {
        if (self::coordinateIsRange($pCoordinateString)) {
            throw new Exception('Cell coordinate string can not be a range of cells');
        }

        list($worksheet, $pCoordinateString) = Worksheet::extractSheetTitle($pCoordinateString, true);
        if ($worksheet > '') {
            $worksheet .= '!';
        }

        if (ctype_digit($pCoordinateString)) {
            return $worksheet . '$' . $pCoordinateString;
        } elseif (ctype_alpha($pCoordinateString)) {
            return $worksheet . '$' . strtoupper($pCoordinateString);
        }

        return $worksheet . self::absoluteCoordinate($pCoordinateString);
    }

    public static function absoluteCoordinate($pCoordinateString)
    {
        if (self::coordinateIsRange($pCoordinateString)) {
            throw new Exception('Cell coordinate string can not be a range of cells');
        }

        list($worksheet, $pCoordinateString) = Worksheet::extractSheetTitle($pCoordinateString, true);
        if ($worksheet > '') {
            $worksheet .= '!';
        }

        list($column, $row) = self::coordinateFromString($pCoordinateString);
        $column = ltrim($column, '$');
        $row = ltrim($row, '$');

        return $worksheet . '$' . $column . '$' . $row;
    }

    public static function splitRange($pRange)
    {
        if (empty($pRange)) {
            $pRange = self::DEFAULT_RANGE;
        }

        $exploded = explode(',', $pRange);
        $counter = count($exploded);
        for ($i = 0; $i < $counter; ++$i) {
            $exploded[$i] = explode(':', $exploded[$i]);
        }

        return $exploded;
    }

    public static function buildRange(array $pRange)
    {
        if (empty($pRange) || !is_array($pRange[0])) {
            throw new Exception('Range does not contain any information');
        }

        $imploded = [];
        $counter = count($pRange);
        for ($i = 0; $i < $counter; ++$i) {
            $pRange[$i] = implode(':', $pRange[$i]);
        }
        $imploded = implode(',', $pRange);

        return $imploded;
    }

    public static function rangeBoundaries($pRange)
    {
        if (empty($pRange)) {
            $pRange = self::DEFAULT_RANGE;
        }

        $pRange = strtoupper($pRange);

        if (strpos($pRange, ':') === false) {
            $rangeA = $rangeB = $pRange;
        } else {
            list($rangeA, $rangeB) = explode(':', $pRange);
        }

        $rangeStart = self::coordinateFromString($rangeA);
        $rangeEnd = self::coordinateFromString($rangeB);

        $rangeStart[0] = self::columnIndexFromString($rangeStart[0]);
        $rangeEnd[0] = self::columnIndexFromString($rangeEnd[0]);

        return [$rangeStart, $rangeEnd];
    }

    public static function rangeDimension($pRange)
    {
        list($rangeStart, $rangeEnd) = self::rangeBoundaries($pRange);

        return [($rangeEnd[0] - $rangeStart[0] + 1), ($rangeEnd[1] - $rangeStart[1] + 1)];
    }

    public static function getRangeBoundaries($pRange)
    {
        if (empty($pRange)) {
            $pRange = self::DEFAULT_RANGE;
        }

        $pRange = strtoupper($pRange);

        if (strpos($pRange, ':') === false) {
            $rangeA = $rangeB = $pRange;
        } else {
            list($rangeA, $rangeB) = explode(':', $pRange);
        }

        return [self::coordinateFromString($rangeA), self::coordinateFromString($rangeB)];
    }

    public static function columnIndexFromString($pString)
    {
        static $indexCache = [];

        if (isset($indexCache[$pString])) {
            return $indexCache[$pString];
        }
        static $columnLookup = [
            'A' => 1, 'B' => 2, 'C' => 3, 'D' => 4, 'E' => 5, 'F' => 6, 'G' => 7, 'H' => 8, 'I' => 9, 'J' => 10, 'K' => 11, 'L' => 12, 'M' => 13,
            'N' => 14, 'O' => 15, 'P' => 16, 'Q' => 17, 'R' => 18, 'S' => 19, 'T' => 20, 'U' => 21, 'V' => 22, 'W' => 23, 'X' => 24, 'Y' => 25, 'Z' => 26,
            'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5, 'f' => 6, 'g' => 7, 'h' => 8, 'i' => 9, 'j' => 10, 'k' => 11, 'l' => 12, 'm' => 13,
            'n' => 14, 'o' => 15, 'p' => 16, 'q' => 17, 'r' => 18, 's' => 19, 't' => 20, 'u' => 21, 'v' => 22, 'w' => 23, 'x' => 24, 'y' => 25, 'z' => 26,
        ];

        if (isset($pString[0])) {
            if (!isset($pString[1])) {
                $indexCache[$pString] = $columnLookup[$pString];

                return $indexCache[$pString];
            } elseif (!isset($pString[2])) {
                $indexCache[$pString] = $columnLookup[$pString[0]] * 26 + $columnLookup[$pString[1]];

                return $indexCache[$pString];
            } elseif (!isset($pString[3])) {
                $indexCache[$pString] = $columnLookup[$pString[0]] * 676 + $columnLookup[$pString[1]] * 26 + $columnLookup[$pString[2]];

                return $indexCache[$pString];
            }
        }

        throw new Exception('Column string index can not be ' . ((isset($pString[0])) ? 'longer than 3 characters' : 'empty'));
    }

    public static function stringFromColumnIndex($columnIndex)
    {
        static $indexCache = [];

        if (!isset($indexCache[$columnIndex])) {
            $indexValue = $columnIndex;
            $base26 = null;
            do {
                $characterValue = ($indexValue % 26) ?: 26;
                $indexValue = ($indexValue - $characterValue) / 26;
                $base26 = chr($characterValue + 64) . ($base26 ?: '');
            } while ($indexValue > 0);
            $indexCache[$columnIndex] = $base26;
        }

        return $indexCache[$columnIndex];
    }

    public static function extractAllCellReferencesInRange($pRange)
    {
        $returnValue = [];

        $cellBlocks = self::getCellBlocksFromRangeString($pRange);
        foreach ($cellBlocks as $cellBlock) {
            $returnValue = array_merge($returnValue, self::getReferencesForCellBlock($cellBlock));
        }

        $sortKeys = [];
        foreach (array_unique($returnValue) as $coord) {
            $column = '';
            $row = 0;

            sscanf($coord, '%[A-Z]%d', $column, $row);
            $sortKeys[sprintf('%3s%09d', $column, $row)] = $coord;
        }
        ksort($sortKeys);

        return array_values($sortKeys);
    }

    private static function getReferencesForCellBlock($cellBlock)
    {
        $returnValue = [];

        if (!self::coordinateIsRange($cellBlock)) {
            return (array) $cellBlock;
        }

        $ranges = self::splitRange($cellBlock);
        foreach ($ranges as $range) {
            if (!isset($range[1])) {
                $returnValue[] = $range[0];

                continue;
            }

            list($rangeStart, $rangeEnd) = $range;
            list($startColumn, $startRow) = self::coordinateFromString($rangeStart);
            list($endColumn, $endRow) = self::coordinateFromString($rangeEnd);
            $startColumnIndex = self::columnIndexFromString($startColumn);
            $endColumnIndex = self::columnIndexFromString($endColumn);
            ++$endColumnIndex;

            $currentColumnIndex = $startColumnIndex;
            $currentRow = $startRow;

            self::validateRange($cellBlock, $startColumnIndex, $endColumnIndex, $currentRow, $endRow);

            while ($currentColumnIndex < $endColumnIndex) {
                while ($currentRow <= $endRow) {
                    $returnValue[] = self::stringFromColumnIndex($currentColumnIndex) . $currentRow;
                    ++$currentRow;
                }
                ++$currentColumnIndex;
                $currentRow = $startRow;
            }
        }

        return $returnValue;
    }

    public static function mergeRangesInCollection(array $pCoordCollection)
    {
        $hashedValues = [];
        $mergedCoordCollection = [];

        foreach ($pCoordCollection as $coord => $value) {
            if (self::coordinateIsRange($coord)) {
                $mergedCoordCollection[$coord] = $value;

                continue;
            }

            list($column, $row) = self::coordinateFromString($coord);
            $row = (int) (ltrim($row, '$'));
            $hashCode = $column . '-' . (is_object($value) ? $value->getHashCode() : $value);

            if (!isset($hashedValues[$hashCode])) {
                $hashedValues[$hashCode] = (object) [
                    'value' => $value,
                    'col' => $column,
                    'rows' => [$row],
                ];
            } else {
                $hashedValues[$hashCode]->rows[] = $row;
            }
        }

        ksort($hashedValues);

        foreach ($hashedValues as $hashedValue) {
            sort($hashedValue->rows);
            $rowStart = null;
            $rowEnd = null;
            $ranges = [];

            foreach ($hashedValue->rows as $row) {
                if ($rowStart === null) {
                    $rowStart = $row;
                    $rowEnd = $row;
                } elseif ($rowEnd === $row - 1) {
                    $rowEnd = $row;
                } else {
                    if ($rowStart == $rowEnd) {
                        $ranges[] = $hashedValue->col . $rowStart;
                    } else {
                        $ranges[] = $hashedValue->col . $rowStart . ':' . $hashedValue->col . $rowEnd;
                    }

                    $rowStart = $row;
                    $rowEnd = $row;
                }
            }

            if ($rowStart !== null) {
                if ($rowStart == $rowEnd) {
                    $ranges[] = $hashedValue->col . $rowStart;
                } else {
                    $ranges[] = $hashedValue->col . $rowStart . ':' . $hashedValue->col . $rowEnd;
                }
            }

            foreach ($ranges as $range) {
                $mergedCoordCollection[$range] = $hashedValue->value;
            }
        }

        return $mergedCoordCollection;
    }

    private static function getCellBlocksFromRangeString($pRange)
    {
        return explode(' ', str_replace('$', '', strtoupper($pRange)));
    }

    private static function validateRange($cellBlock, $startColumnIndex, $endColumnIndex, $currentRow, $endRow)
    {
        if ($startColumnIndex >= $endColumnIndex || $currentRow > $endRow) {
            throw new Exception('Invalid range: "' . $cellBlock . '"');
        }
    }
}



class DataType
{
    const TYPE_STRING2 = 'str';
    const TYPE_STRING = 's';
    const TYPE_FORMULA = 'f';
    const TYPE_NUMERIC = 'n';
    const TYPE_BOOL = 'b';
    const TYPE_NULL = 'null';
    const TYPE_INLINE = 'inlineStr';
    const TYPE_ERROR = 'e';

    private static $errorCodes = [
        '#NULL!' => 0,
        '#DIV/0!' => 1,
        '#VALUE!' => 2,
        '#REF!' => 3,
        '#NAME?' => 4,
        '#NUM!' => 5,
        '#N/A' => 6,
    ];

    public static function getErrorCodes()
    {
        return self::$errorCodes;
    }

    public static function checkString($pValue)
    {
        if ($pValue instanceof RichText) {
            return $pValue;
        }

        $pValue = StringHelper::substring($pValue, 0, 32767);

        $pValue = str_replace(["\r\n", "\r"], "\n", $pValue);

        return $pValue;
    }

    public static function checkErrorCode($pValue)
    {
        $pValue = (string) $pValue;

        if (!isset(self::$errorCodes[$pValue])) {
            $pValue = '#NULL!';
        }

        return $pValue;
    }
}

class DefaultValueBinder implements IValueBinder
{
    public function bindValue(Cell $cell, $value)
    {
        if (is_string($value)) {
            $value = StringHelper::sanitizeUTF8($value);
        } elseif (is_object($value)) {
            if ($value instanceof DateTimeInterface) {
                $value = $value->format('Y-m-d H:i:s');
            } elseif (!($value instanceof RichText)) {
                $value = (string) $value;
            }
        }

        $cell->setValueExplicit($value, static::dataTypeForValue($value));

        return true;
    }

    public static function dataTypeForValue($pValue)
    {
        if ($pValue === null) {
            return DataType::TYPE_NULL;
        } elseif ($pValue === '') {
            return DataType::TYPE_STRING;
        } elseif ($pValue instanceof RichText) {
            return DataType::TYPE_INLINE;
        } elseif ($pValue[0] === '=' && strlen($pValue) > 1) {
            return DataType::TYPE_FORMULA;
        } elseif (is_bool($pValue)) {
            return DataType::TYPE_BOOL;
        } elseif (is_float($pValue) || is_int($pValue)) {
            return DataType::TYPE_NUMERIC;
        } elseif (preg_match('/^[\+\-]?(\d+\\.?\d*|\d*\\.?\d+)([Ee][\-\+]?[0-2]?\d{1,3})?$/', $pValue)) {
            $tValue = ltrim($pValue, '+-');
            if (is_string($pValue) && $tValue[0] === '0' && strlen($tValue) > 1 && $tValue[1] !== '.') {
                return DataType::TYPE_STRING;
            } elseif ((strpos($pValue, '.') === false) && ($pValue > PHP_INT_MAX)) {
                return DataType::TYPE_STRING;
            }

            return DataType::TYPE_NUMERIC;
        } elseif (is_string($pValue)) {
            $errorCodes = DataType::getErrorCodes();
            if (isset($errorCodes[$pValue])) {
                return DataType::TYPE_ERROR;
            }
        }

        return DataType::TYPE_STRING;
    }
}

interface IValueBinder
{
    public function bindValue(Cell $cell, $value);
}



class Hyperlink
{
    private $url;

    private $tooltip;

    public function __construct($pUrl = '', $pTooltip = '')
    {
        $this->url = $pUrl;
        $this->tooltip = $pTooltip;
    }

    public function getUrl()
    {
        return $this->url;
    }

    public function setUrl($value)
    {
        $this->url = $value;

        return $this;
    }

    public function getTooltip()
    {
        return $this->tooltip;
    }

    public function setTooltip($value)
    {
        $this->tooltip = $value;

        return $this;
    }

    public function isInternal()
    {
        return strpos($this->url, 'sheet://') !== false;
    }

    public function getTypeHyperlink()
    {
        return $this->isInternal() ? '' : 'External';
    }

    public function getHashCode()
    {
        return md5(
            $this->url .
            $this->tooltip .
            __CLASS__
        );
    }
}


class DataValidation
{
    const TYPE_NONE = 'none';
    const TYPE_CUSTOM = 'custom';
    const TYPE_DATE = 'date';
    const TYPE_DECIMAL = 'decimal';
    const TYPE_LIST = 'list';
    const TYPE_TEXTLENGTH = 'textLength';
    const TYPE_TIME = 'time';
    const TYPE_WHOLE = 'whole';

    const STYLE_STOP = 'stop';
    const STYLE_WARNING = 'warning';
    const STYLE_INFORMATION = 'information';

    const OPERATOR_BETWEEN = 'between';
    const OPERATOR_EQUAL = 'equal';
    const OPERATOR_GREATERTHAN = 'greaterThan';
    const OPERATOR_GREATERTHANOREQUAL = 'greaterThanOrEqual';
    const OPERATOR_LESSTHAN = 'lessThan';
    const OPERATOR_LESSTHANOREQUAL = 'lessThanOrEqual';
    const OPERATOR_NOTBETWEEN = 'notBetween';
    const OPERATOR_NOTEQUAL = 'notEqual';

    private $formula1 = '';

    private $formula2 = '';

    private $type = self::TYPE_NONE;

    private $errorStyle = self::STYLE_STOP;

    private $operator = self::OPERATOR_BETWEEN;

    private $allowBlank = false;

    private $showDropDown = false;

    private $showInputMessage = false;

    private $showErrorMessage = false;

    private $errorTitle = '';

    private $error = '';

    private $promptTitle = '';

    private $prompt = '';

    public function __construct()
    {
    }

    public function getFormula1()
    {
        return $this->formula1;
    }

    public function setFormula1($value)
    {
        $this->formula1 = $value;

        return $this;
    }

    public function getFormula2()
    {
        return $this->formula2;
    }

    public function setFormula2($value)
    {
        $this->formula2 = $value;

        return $this;
    }

    public function getType()
    {
        return $this->type;
    }

    public function setType($value)
    {
        $this->type = $value;

        return $this;
    }

    public function getErrorStyle()
    {
        return $this->errorStyle;
    }

    public function setErrorStyle($value)
    {
        $this->errorStyle = $value;

        return $this;
    }

    public function getOperator()
    {
        return $this->operator;
    }

    public function setOperator($value)
    {
        $this->operator = $value;

        return $this;
    }

    public function getAllowBlank()
    {
        return $this->allowBlank;
    }

    public function setAllowBlank($value)
    {
        $this->allowBlank = $value;

        return $this;
    }

    public function getShowDropDown()
    {
        return $this->showDropDown;
    }

    public function setShowDropDown($value)
    {
        $this->showDropDown = $value;

        return $this;
    }

    public function getShowInputMessage()
    {
        return $this->showInputMessage;
    }

    public function setShowInputMessage($value)
    {
        $this->showInputMessage = $value;

        return $this;
    }

    public function getShowErrorMessage()
    {
        return $this->showErrorMessage;
    }

    public function setShowErrorMessage($value)
    {
        $this->showErrorMessage = $value;

        return $this;
    }

    public function getErrorTitle()
    {
        return $this->errorTitle;
    }

    public function setErrorTitle($value)
    {
        $this->errorTitle = $value;

        return $this;
    }

    public function getError()
    {
        return $this->error;
    }

    public function setError($value)
    {
        $this->error = $value;

        return $this;
    }

    public function getPromptTitle()
    {
        return $this->promptTitle;
    }

    public function setPromptTitle($value)
    {
        $this->promptTitle = $value;

        return $this;
    }

    public function getPrompt()
    {
        return $this->prompt;
    }

    public function setPrompt($value)
    {
        $this->prompt = $value;

        return $this;
    }

    public function getHashCode()
    {
        return md5(
            $this->formula1 .
            $this->formula2 .
            $this->type .
            $this->errorStyle .
            $this->operator .
            ($this->allowBlank ? 't' : 'f') .
            ($this->showDropDown ? 't' : 'f') .
            ($this->showInputMessage ? 't' : 'f') .
            ($this->showErrorMessage ? 't' : 'f') .
            $this->errorTitle .
            $this->error .
            $this->promptTitle .
            $this->prompt .
            __CLASS__
        );
    }

    public function __clone()
    {
        $vars = get_object_vars($this);
        foreach ($vars as $key => $value) {
            if (is_object($value)) {
                $this->$key = clone $value;
            } else {
                $this->$key = $value;
            }
        }
    }
}

